feat(android): Add internal non-terminating envelope capture #5795
feat(android): Add internal non-terminating envelope capture #5795buenaflor wants to merge 11 commits into
Conversation
…-unhandled sessions Hybrid unhandled exceptions that don't terminate the process (e.g. Flutter) no longer end the session as crashed. The session is kept alive and marked pending-unhandled, finalized as unhandled on session end or escalated to crashed on a native crash. Existing captureEnvelope(byte[], boolean) behavior (React Native) is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the existing hybrid capture path isolated from non-terminating Flutter session handling while retaining pending-unhandled session persistence. Co-authored-by: Cursor <cursoragent@cursor.com>
Serialize pending session mutations with scope and cache lifecycle operations so delayed session updates cannot overwrite or delete newer session state. Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Added unreleased section with recent fixes for ANR and crash events.
|
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c6f705c. Configure here.
📲 Install BuildsAndroid
|
|
@sentry review |
Check for an unhandled exception directly instead of using the crash-named predicate in a path where the process intentionally continues. Co-authored-by: Cursor <cursoragent@cursor.com>
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| eb95ded | 317.51 ms | 369.08 ms | 51.57 ms |
| 2124a46 | 319.19 ms | 415.04 ms | 95.85 ms |
| c3ee041 | 310.64 ms | 361.90 ms | 51.26 ms |
| dcc6bbf | 382.58 ms | 462.13 ms | 79.54 ms |
| 7a19fee | 315.46 ms | 368.62 ms | 53.16 ms |
| 9054d65 | 330.94 ms | 403.24 ms | 72.30 ms |
| d500866 | 326.13 ms | 378.70 ms | 52.58 ms |
| ad8da22 | 365.86 ms | 427.00 ms | 61.14 ms |
| ed33deb | 337.52 ms | 484.06 ms | 146.54 ms |
| d15471f | 310.26 ms | 377.04 ms | 66.78 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| eb95ded | 0 B | 0 B | 0 B |
| 2124a46 | 1.58 MiB | 2.12 MiB | 551.51 KiB |
| c3ee041 | 0 B | 0 B | 0 B |
| dcc6bbf | 1.58 MiB | 2.12 MiB | 553.10 KiB |
| 7a19fee | 0 B | 0 B | 0 B |
| 9054d65 | 1.58 MiB | 2.29 MiB | 723.38 KiB |
| d500866 | 0 B | 0 B | 0 B |
| ad8da22 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| ed33deb | 1.58 MiB | 2.13 MiB | 559.52 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
Only preserve a mismatched current session when it carries pending unhandled state, retaining existing deletion behavior for other sessions. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@sentry review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 184e89a. Configure here.
There was a problem hiding this comment.
Pull request overview
This PR adds a non-terminating “unhandled” session path intended for hybrid runtimes (notably Flutter) where handled=false does not imply a process crash. It introduces a locally persisted “pending unhandled” marker on the active session, so the session can later be finalized as unhandled on natural end (unless escalated to crashed/abnormal).
Changes:
- Add
InternalSentrySdk.captureEnvelopeNonTerminating(byte[])(Android) to capture envelopes without ending/replacing the session, while persisting a pending-unhandled marker and incrementing session error count. - Extend
Sessionto support a pending-unhandled lifecycle (mutation, cloning, serialization) and adjust session end/finalization behavior. - Update
EnvelopeCachesession start/end handling to preserve newer same-SID snapshots and avoid deleting a pending session on mismatched session-end envelopes; add broad test coverage across cache/finalizer/session serialization.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| sentry/src/main/java/io/sentry/Session.java | Adds pending-unhandled state tracking, serialization/deserialization, and end()/update() precedence rules. |
| sentry/src/main/java/io/sentry/Scope.java | Exposes Scope.IWithSession as public (still @ApiStatus.Internal) to enable cross-module usage. |
| sentry/src/main/java/io/sentry/cache/EnvelopeCache.java | Improves session start/end disk behavior for same-SID delayed snapshots and pending-unhandled preservation; adds persistCurrentSession. |
| sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java | Adds captureEnvelopeNonTerminating and documents when to use it vs terminating capture. |
| sentry/src/test/java/io/sentry/SessionTest.kt | Adds unit tests for pending-unhandled mutation, end-state precedence, clone behavior, and JSON round-tripping. |
| sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt | Adds protocol-level serialization test for unhandled + pending flag round-trip. |
| sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt | Adds recovery/finalization tests for pending-unhandled vs crash-marker precedence. |
| sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt | Adds extensive session file behavior tests for SID matching/mismatch, null SID edge cases, and terminal precedence. |
| sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt | Adds integration-style tests validating non-terminating behavior, persistence, and later finalization/escalation. |
| sentry/api/sentry.api | Updates API surface snapshots for new/changed public ABI elements. |
| sentry-android-core/api/sentry-android-core.api | Updates API snapshot to include captureEnvelopeNonTerminating. |
| CHANGELOG.md | Adds an “Internal” changelog entry describing the new hybrid non-terminating capture API. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Only retain a mismatched pending session when its start time proves it is newer than the delayed session end. Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
| try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { | ||
| final @Nullable Session endingSession = readSessionFromEnvelope(envelope); | ||
| final @Nullable Session currentSession = readSessionFromDisk(currentSessionFile); | ||
| final boolean preservePendingSession = | ||
| endingSession != null | ||
| && currentSession != null | ||
| && currentSession.isPendingUnhandled() | ||
| && endingSession.getSessionId() != null | ||
| && currentSession.getSessionId() != null | ||
| && !Objects.equals(endingSession.getSessionId(), currentSession.getSessionId()) | ||
| && endingSession.getStarted() != null | ||
| && currentSession.getStarted() != null | ||
| && currentSession.getStarted().after(endingSession.getStarted()); | ||
| if (!preservePendingSession && !currentSessionFile.delete()) { | ||
| options.getLogger().log(WARNING, "Current envelope doesn't exist."); |
There was a problem hiding this comment.
Instead of deleting session.json unconditionally, we preserve it when a delayed End(A) would delete a newer pending-unhandled session B, confirmed by a different SID and later start time. For example, if End(A) is queued before B’s pending-unhandled state is written, processing End(A) must not delete B’s file. All other cases retain the existing deletion behavior, so this currently only affects state persisted by captureEnvelopeNonTerminating().
📜 Description
Add
InternalSentrySdk.captureEnvelopeNonTerminatingfor hybrid runtimes such as Flutter where an unhandled exception does not terminate the process.The new path keeps the current session alive with the same SID, increments its error count, persists a pending-unhandled marker in the existing session cache, and finalizes it as
unhandledonly when the session naturally ends. ExistingAbnormalandCrashedterminal states remain authoritative, and the existingcaptureEnvelope(byte[], boolean)behavior used by other SDKs is unchanged.💡 Motivation and Context
Flutter currently forwards
handled=falseevents through the terminating hybrid capture path. This marks the session ascrashedand may start a replacement session even though the Flutter process continues running, which lowers crash-free session rates incorrectly.This follows the session protocol's
unhandledstatus for exceptions that are unhandled by the application but do not terminate the process.💚 How did you test it?
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
captureEnvelopeNonTerminatingfor non-terminating unhandled events.Made with Cursor